-
Notifications
You must be signed in to change notification settings - Fork 512
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix: Referencer, Name conflicts, Mapped unions, etc. #1498
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hello there nemnandor 👋
Thank you and congrats 🎉 for opening your first PR on this project.✨
We will review the following PR soon! 👀
@@ -1119,7 +1137,7 @@ describe('Definition generation', () => { | |||
{ | |||
properties: { | |||
list: { | |||
items: { $ref: '#/definitions/ThingContainerWithTitle_string_' }, | |||
items: { type: 'number', format: 'double' }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Equivalent change, but maybe the previous solution was better readable
@@ -1169,13 +1188,13 @@ describe('Definition generation', () => { | |||
format: undefined, | |||
}, | |||
indexedResponseObject: { | |||
$ref: '#/definitions/Record_id.any_', | |||
$ref: '#/definitions/Record_id._myProp1-string__', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Previous name generation worked well only if we were not in a nested mapped type. Fixed to check&handle name conflicts
description: undefined, | ||
example: undefined, | ||
format: undefined, | ||
}, | ||
indexedType: { type: 'string', default: undefined, description: undefined, format: undefined, example: undefined }, | ||
indexedTypeToAlias: { $ref: '#/definitions/IndexedInterfaceAlias', description: undefined, format: undefined, example: undefined }, | ||
indexedTypeToAlias: { $ref: '#/definitions/IndexedInterface', description: undefined, format: undefined, example: undefined }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Equivalent change, but maybe previous was better readable in some cases
@@ -131,7 +131,7 @@ describe('Definition generation', () => { | |||
'TestSubModelContainerNamespace.InnerNamespace.TestSubModelContainer2', | |||
'TestSubModel2', | |||
'TestSubModelNamespace.TestSubModelNS', | |||
'TsoaTest.TestModel73', | |||
'tsoaTest.TsoaTest.TestModel73', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For safe name conflict handling, every type needs to use its fully namespaced name. Not accepted to think, that TsoaTest type is equal to another interface in the tsoaTest namespace
@@ -564,6 +564,7 @@ describe('Definition generation', () => { | |||
default: undefined, | |||
}, | |||
}, | |||
required: ['record-foo', 'record-bar'], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wrong previous tests
@@ -834,7 +852,7 @@ describe('Definition generation', () => { | |||
example: 42, | |||
minimum: 42, | |||
maximum: 42, | |||
default: '42', | |||
default: 42, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now default types are ok, so we can use default values with @default jsdoc comment
@@ -11,7 +11,7 @@ export class GenerateMetadataError extends Error { | |||
} | |||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Small thing, do not crash while trying to make exceptions to throw (or when make warning messages).
@@ -35,7 +36,6 @@ export class MetadataGenerator { | |||
|
|||
this.checkForMethodSignatureDuplicates(controllers); | |||
this.checkForPathParamSignatureDuplicates(controllers); | |||
this.circularDependencyResolvers.forEach(c => c(this.referenceTypeMap)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Handled in another place
} | ||
this.referenceTypeMap[decodeURIComponent(referenceType.refName)] = referenceType; | ||
this.referenceTypeMap[referenceType.refName] = referenceType; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
decodeURIComponent() not interesting here, because typeResolver guarantees, that only OpenApi-accepted names can be generated.
} | ||
|
||
public GetReferenceType(refName: string) { | ||
return this.referenceTypeMap[refName]; | ||
} | ||
|
||
public OnFinish(callback: (referenceTypes: Tsoa.ReferenceTypeMap) => void) { | ||
this.circularDependencyResolvers.push(callback); | ||
public CheckModelUnicity(refName: string, positions: Array<{ fileName: string; pos: number }>) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Type unicity check:
- We want to check, that type A<B<...>,{ c: C}> is uniq or not. For this, we need to check if every A, B & C type identifier definitions are uniq or not. Uniq = A type can have more than one definition, but their definition positions array must be uniq. Position = fileName + pos. Not stored, so not interesting, which local machine the check is made.
- A and A_B_ types are both renamed to A_B_ to use it in OpenApi. This is a second type of name conflicts.
jsonCharacters: undefined; //type is not really interesting | ||
}; | ||
|
||
jsDocTypeNames?: { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This types are different because of their jsdoc comments. Should generate different TSOA types for it (with, of course, different names).
Test checks situations which were handled previously as Partial_any_ to cover a more special situation
}>; | ||
}; | ||
|
||
jsdocMap?: { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shows how to change jsdocs while changing the interface with mapped types
synonym2: JsDoccedSynonym2; | ||
}; | ||
|
||
duplicatedDefinitions?: { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
identifiers with more than one definition
[name: string]: ts.TypeReferenceNode | ts.TypeNode; | ||
[name: string]: { | ||
type: ts.TypeNode; | ||
name: string; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
name is only for cache
description: this.getNodeDescription(propertySignature), | ||
format: this.getNodeFormat(propertySignature), | ||
name: (propertySignature.name as ts.Identifier).text, | ||
required: !propertySignature.questionToken, | ||
required: !propertySignature.questionToken && def === undefined, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If a type has default value, then it can not be required.
If a string has a number default value (for example, because of a type mapping), then no problem, the not defined value will be replaced with the number, and after that the typeCheck says, it is not the expected type.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How does this interact with: #1454
My approach was to leave that up to the mapping of internal types -> OpenAPI 2/3, while keeping the metadata rather unrefined and applying "intentions" later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, I removed the automatic non-required if there is a default value.
const propertyType = this.current.typeChecker.getTypeOfSymbol(property); | ||
|
||
const typeNode = this.current.typeChecker.typeToTypeNode(propertyType, undefined, ts.NodeBuilderFlags.NoTruncation)!; | ||
const parent = getOneOrigDeclaration(property); //If there are more declarations, we need to get one of them, from where we want to recognize jsDoc |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If there are more than one declaration for a type member, then choose one of them for handling jsdoc.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In all other cases we throw if there are multiple. This seems like an intentional deviation?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Currently, tsoa gets the [0]-th element. I think my code is not worse than the original.
Of course, I can make, that the parent member of TypeResolver is not a single value, but an array, and everywhere we want to use it, we check if every elements gets the same result or not. If you think it makes a better world.
|
||
const name = this.contextualizedName(resolvableName); | ||
private calcMemberJsDocProperties(arg: ts.PropertySignature): string { | ||
const def = TypeResolver.getDefault(arg); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If two types differ only in jsdoc, then their name needs to differ, too. Because of it, I need to inject the jsdoc parameters into the type name. The name syntax can not be a valid tsoa syntax, but it's not interesting
let textStartCharacter: `"` | "'" | '`' | undefined = undefined; | ||
const inString = () => textStartCharacter !== undefined; | ||
|
||
let formattedStr = ''; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
#623 JSON not easily supports escaped characters, and only strings with double quotation marks, so preformat is needed
return { | ||
dataType: 'enum', | ||
enums: [null], | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
undefined & null types are interesting if strictNullChecks flag is true in ts compilerOptions
Can you take a look at CI? Let's start there. |
// keyof | ||
if (ts.isTypeOperatorNode(this.typeNode) && this.typeNode.operator === ts.SyntaxKind.KeyOfKeyword) { | ||
const type = this.current.typeChecker.getTypeFromTypeNode(this.typeNode); | ||
if (type.getFlags() & ts.TypeFlags.Index) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a helper that makes this typesafe without casting?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good notice, I have fixed it.
// in case of generic: keyof T. Not handles all possible cases | ||
const symbol = (type as ts.IndexType).type.getSymbol(); | ||
if (symbol && symbol.getFlags() & ts.TypeFlags.TypeParameter) { | ||
const typeName = symbol.getEscapedName(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
add: assert(typeof typeName === 'string')
Validates and avoids casts
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed
LGTM, thanks for the PR and sorry for the time it took to review. |
All Submissions:
Closing issues
closes #477
closes #623
closes #681
closes #1191
closes #1204
closes #1266
closes #1447
closes #1472
If this is a new feature submission:
Fixes:
I have solved locally some problems which are needed for my company. After that I read the TSOA commit guides, so some fixes maybe have no related issues.
@default true
is not a string, but a boolean.Potential Problems With The Approach
allOf
type #1490 is a bug, then I made bugs). Check original test changes.type A = Partial<{ [a in keyof B]: string }>
But there is a workaround:
type AInternal = { [a in keyof B]: string }; type A = Partial<AInternal>
I'm sure, that there are a lot of breaking changes.
Test plan
Added test type literals to the TestModel interface. Every literal checks one solved problem. The members represents one test case.